package br.gov.cemaden.sendmail;

import br.gov.cemaden.sendmail.exception.SendmailConfigurationException;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.activation.FileDataSource;
import javax.mail.Authenticator;
import javax.mail.BodyPart;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;

/**
 *
 * @author alan
 */
public class Sendmail {
    
    private final String subject;
    private final String body;
    private String textMimeType;
    private String host, email, password, from;
    private final Set<String> recipients = new CopyOnWriteArraySet<String>();
    private Set<String> attachments = null;
    private Integer port;    
    private Protocol protocol = Protocol.SMTP;
    private boolean auth;
    private boolean debug;
    
    /**
     * Creates an environment for sending e-mails with basic data.
     * 
     * @param subject subject of the message
     * @param body body (content) of the message
     */
    public Sendmail(String subject, String body){
        this.subject = subject;
        this.body = body;
        /* Default values. Can be changed via set methods. */
        auth = true;
        debug = true;
    }
    
    /**
     * Creates an environment for sending e-mails with basic data and properties file data to fill up send e-mails requirements.     
     * 
     * @param subject subject of the message
     * @param body body (content) of the message
     * @param path a properties file containing the key/value pairs to fill the class attributes. This attributes can be set again in it's respective methods.
     * @throws java.io.FileNotFoundException
     * @throws java.io.IOException
     */
    public Sendmail(String subject, String body, Path path) throws FileNotFoundException, IOException{
        this(subject, body);
        Properties props = new Properties();
        try {
            props.load(new FileInputStream(path.toFile()));
        } catch (FileNotFoundException ex) {
            throw new FileNotFoundException(String.format("The specified file - %s - was not found.%n%s", path.toString(), ex.getMessage()));
        } catch (IOException ex) {
            throw new IOException(String.format("Error when trying to read the specified file %s%n%s", path.toString(), ex.getMessage()));
        }
        /* auth, debug and other variables are read here */
        loadProperties(props);
    }    
    
    /**
     * Sets the mime type to be used in the email message. The possible values are text/html or text/html and text/plain, the latter being the default value.
     * @param textMimeType
     * @return the actual {@link Sendmail} object
     */
    public Sendmail setBodyMimeType(String textMimeType){
        this.textMimeType = textMimeType;
        return this;
    }
    
    /**
     * 
     * @param host the host by which the email is sent
     * @return the actual {@link Sendmail} object
     */
    public Sendmail setHost(String host){
        this.host = host;
        return this;
    }
    
    /**
     * 
     * @return the host being set
     */
    public String getHost(){
        return this.host;
    }
    
    /**
     * 
     * @param port
     * @return the actual {@link Sendmail} object
     */
    public Sendmail setPort(int port){
        this.port = port;
        return this;
    }
    
    /**
     * 
     * @return the host port being used
     */
    public int getPort(){
        return this.port;
    }
    
    /**
     * 
     * @param email
     * @return the actual {@link Sendmail} object
     */
    public Sendmail setEmail(String email){
        this.email = email;
        return this;
    }
    
    /**
     * 
     * @return the email being used to send the email
     */
    public String getEmail(){
        return this.email;
    }
    
    /**
     * 
     * @param password
     * @return the actual {@link Sendmail} object
     */
    public Sendmail setPassword(String password){
        this.password = password;
        return this;
    }
    
    /**
     * 
     * @param from
     * @return the actual {@link Sendmail} object
     */
    public Sendmail setFrom(String from){
        this.from = from;
        return this;
    }
    
    /**
     * 
     * @return from attribute
     */
    public String getFrom(){
        return this.from;
    }
    
    /**
     * 
     * @param recipient
     * @return the actual {@link Sendmail} object
     */
    public Sendmail setRecipients(String recipient){
        if(!recipient.isEmpty())
            this.recipients.add(recipient);
        return this;
    }
    
    /**
     * 
     * @param recipients
     * @return the actual {@link Sendmail} object
     */
    public Sendmail setRecipients(Set<String> recipients){
        if(!recipients.isEmpty())
            this.recipients.addAll(recipients);
        return this;
    }        
    
    /**
     * 
     * @return a set of e-mails ({@code String}) who will receive the email
     */
    public Set<String> getRecipients(){
        return this.recipients;
    }
    
    /**
     * 
     * @param attachment
     * @return
     * @throws FileNotFoundException 
     */
    public Sendmail setAttachments(String attachment) throws FileNotFoundException{
        if(attachments == null){
             attachments = new HashSet<String>();
        }
        Path file = Paths.get(attachment);
        if(Files.notExists(file)){
            throw new FileNotFoundException(String.format("The file %s was not found.", attachment));
        }
        attachments.add(attachment);
        return this;
    }
    
    /**
     * 
     * @param attachments
     * @return
     * @throws FileNotFoundException 
     */
    public Sendmail setAttachments(Set<String> attachments) throws FileNotFoundException{
        if(this.attachments == null){
            this.attachments = new HashSet<String>();
        }
        for(String attachment : attachments){
            Path file = Paths.get(attachment);
            if(Files.notExists(file)){
                throw new FileNotFoundException(String.format("The file %s was not found", attachment));
            }
            this.attachments.add(attachment);
        }
        return this;
    }
    
    /**
     * 
     * @return 
     */
    public Set<String> getAttachments(){
        return this.attachments;
    }
    
    /**
     * 
     * @param protocol
     * @return the actual {@link Sendmail} object
     */
    public Sendmail setProtocol(Protocol protocol){
        this.protocol = protocol;
        return this;
    }
    
    /**
     * 
     * @return the {@link Protocol} being used
     */
    public Protocol getProtocol(){
        return this.protocol;
    }
    
    /**
     * 
     * @param auth
     * @return the actual {@link Sendmail} object
     */
    public Sendmail setAuth(Boolean auth){
        this.auth = auth;
        return this;
    }
    
    /**
     * @default false
     * @return <tt>true</tt> if authentication is needed
     */
    public Boolean getAuth(){
        return this.auth;
    }
    
    /**
     * Set the debug setting for this object
     * 
     * @param debug
     * @return the actual {@link Sendmail} object
     */
    public Sendmail setDebug(Boolean debug){
        this.debug = debug;
        return this;
    }
    
    /**
     * 
     * @return <tt>true</tt> if the process of sending email will be displayed.
     */
    public Boolean getDebug(){
        return this.debug;
    }
    
    /**
     * Method to send e-mails
     * @throws javax.mail.MessagingException
     * @throws java.io.FileNotFoundException
     * @throws br.gov.cemaden.sendmail.exception.SendmailConfigurationException
     */
    public void send() throws MessagingException, FileNotFoundException, SendmailConfigurationException{
        
        /* First of all, validate attributes */
        validateAttributes();                
                       
        /*
        * Transform e-mails set in an InternetAddress array.
        * First we create an InternetAddress array with our Set<String> recipients size
        * second we transform our Set<String> recipients in an array of Strings (temp)
        * then we iterate over it, putting in each index of InternetAddress array a new InternetAddress object,
        * passing to it the value of the equivalent index of temp array.
        */            
        InternetAddress[] addresses =  new InternetAddress[recipients.size()];            
        String[] temp = recipients.toArray(new String[recipients.size()]);            
        for(int i = 0; i < temp.length; i++){
            try {
                addresses[i] = new InternetAddress(temp[i]);
            } catch (AddressException ex) {
                throw new AddressException(String.format("The e-mail address %s couldn't be parsed.%n%s", temp[i], ex.getMessage()));
            }
        }
        
        /* Creates connection properties */                
        Properties props = new Properties();
        props.put("mail.smtp.host", host);
        props.put("mail.smtp.port", port);
        /* Depending on the Protocol type, an additional propertie is required */
        switch (protocol) {
            case SMTPS:
                props.put("mail.smtp.ssl.enable", true);
                props.put("mail.smtp.ssl.trust", this.host);
                break;
            case TLS:
                props.put("mail.smtp.starttls.enable", true);
                props.put("mail.smtp.ssl.trust", this.host);
                break;
        }
        
        /* Check for the necessity of authentication and, if true, do it. */
        
        Authenticator authenticator = null;
        if (auth) {
            props.put("mail.smtp.auth", true);
            authenticator = new Authenticator() {
                private PasswordAuthentication pa = new PasswordAuthentication(email, password);
                @Override
                public PasswordAuthentication getPasswordAuthentication() {
                    return pa;
                }
            };
        }
        
        /* Creates session basead on properties and authentication */
        
        Session session = Session.getInstance(props, authenticator);
        session.setDebug(debug);
        
        /* Creates MimeMessage object, the one which will be sent over Transport.send(Message message) method  */
        
        MimeMessage message = new MimeMessage(session);
         
        /* Create Multipart object, that will be used to set the content of the message (text or html) 
        and the message attachments if there are */
        
        Multipart multipart = new MimeMultipart();
        
        /* Set MimeMessage attributes */            
        message.setRecipients(Message.RecipientType.TO, addresses);
        message.setFrom(new InternetAddress(this.from));
        message.setSubject(subject);
        message.setSentDate(new Date());
        
        /* Create BodyPart object to add content to message body, depending on text type */
        
        BodyPart messageBody = new MimeBodyPart();
        
        if(textMimeType.equals("text/html") || textMimeType.equals("text/htm"))
            messageBody.setContent(body, "text/html");
        else
            messageBody.setText(body);
        
        /* Add message body content (html or text) to Multipart object */
        multipart.addBodyPart(messageBody);
        
        
        if(attachments != null){            
            for(String attachment : attachments){
                
                Path file = Paths.get(attachment);
                if(Files.notExists(file)){
                    throw new FileNotFoundException(String.format("Error when trying to attach file %s", file.toAbsolutePath().toString()));
                }
                
                MimeBodyPart attachmentPart = new MimeBodyPart();
                
                DataSource source = new FileDataSource(file.toFile());
                attachmentPart.setDataHandler(new DataHandler(source));
                attachmentPart.setFileName(source.getName());
                /* Add attachment to Multipart object */
                multipart.addBodyPart(attachmentPart);
            }
        }
        
        /* Add body and attachments to the message */
        message.setContent(multipart);

        /* Send message */
        Transport.send(message);
    }
    
    /**
     * 
     */
    private void validateAttributes() throws AddressException, SendmailConfigurationException{
        
        if(host == null || host.isEmpty()){
            throw new SendmailConfigurationException("The attribute host is empty.");
        }        
        
        if(port == null || port.toString().isEmpty()){
            throw new SendmailConfigurationException("The attribute port (webmail server port that will be used to send the e-mail) is empty");
        }
        
        if(auth){
            if(email == null || email.isEmpty()){
                throw new SendmailConfigurationException("The attribute email is empty.");
            }
            
            if(password == null || password.isEmpty()){
                throw new SendmailConfigurationException("The attribute password is empty.");
            }                
        }
        
        /* Keep just valid emails in the set */
        for (String recipient : recipients) {
            if(!validateEmail(recipient)){
                recipients.remove(recipient);
            }   
        }
        /* If after validating the e-mails the set is empty, throw an exception */
        if(recipients.isEmpty()){
            throw new SendmailConfigurationException("The email's receivers list is empty.");
        }
        
        /* Validate the email instance variable, if it is not valid the method validateEmail will throw an exception */
        validateEmail(email);        
        
        /* If from instance variable is empty, get the email account value to it. */
        if(from == null || from.isEmpty()){
            from = email;
        } /* If not, validate the from email attribute */
        else{ 
            /* If instance variable from is not valid, use instance variavle email instead of it. */
            try{
                validateEmail(from);
            }
            catch(AddressException ex){
                this.from = email;
                System.out.printf("Changed sendmail.from %s by sendmail.email %s due to:%n", from, email, ex.getMessage());
            }
        }
        
        /* Validate textMimeType */
        if(textMimeType == null || textMimeType.isEmpty()){
            textMimeType = "text/plain";
        }
    }
    
    /**
     * 
     * @param email
     * @return <tt>true</tt> if the email is valid
     */
    private boolean validateEmail(String email) throws AddressException {
        try {
            new InternetAddress(email).validate(); 
        } catch (AddressException ex) {
            StringWriter writer = new StringWriter();
            PrintWriter printer = new PrintWriter(writer);
            ex.printStackTrace(printer);
            throw new AddressException(                
                String.format("Exception when trying to send the email - wrong email address: %s%n%s%n", 
                            email, writer.toString()));
        }
        return true;
    }
    
    /**
     * 
     * @param props 
     */
    private void loadProperties(Properties props){
        this.host = props.getProperty("sendmail.host", "").trim();
        
        this.port = Integer.parseInt(props.getProperty("sendmail.port", "25").trim());
        
        String prtcl = props.getProperty("sendmail.protocol", "SMTP").trim();
        if( prtcl.toUpperCase().equals("SMTPS") ){
            this.protocol = Protocol.SMTPS;
        }
        else if( prtcl.toUpperCase().equals("TLS") ){
            this.protocol = Protocol.TLS;
        }
        
        String authentication = props.getProperty("sendmail.isAuth", "true").toLowerCase().trim();
        this.auth = !authentication.equals("false");
        
        this.email = props.getProperty("sendmail.email", "").trim();
        
        this.password = props.getProperty("sendmail.password", "").trim();
        
        this.from = props.getProperty("sendmail.from", "").trim();
        
        String recipient = props.getProperty("sendmail.default.recipients", "").trim();        
        if(!recipient.isEmpty()){
            if(recipient.contains(";")){
                List<String> list = Arrays.asList(recipient.split(";"));
                this.recipients.addAll(list);
            }
            else{
                this.recipients.add(recipient);            
            }
        }
        
        String debugger = props.getProperty("sendmail.debug", "true").toLowerCase().trim();
        this.debug = !debugger.equals("false");        
    }        
    
}